/*
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package de.chimos.property.compiler.passtwo.javafx;


import static org.objectweb.asm.Opcodes.ACC_FINAL;
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.ASM4;
import static org.objectweb.asm.Opcodes.CHECKCAST;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.F_SAME;
import static org.objectweb.asm.Opcodes.GETFIELD;
import static org.objectweb.asm.Opcodes.IFNONNULL;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.NEW;
import static org.objectweb.asm.Opcodes.PUTFIELD;
import static org.objectweb.asm.Opcodes.RETURN;

import java.util.HashMap;
import java.util.Map;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;

import de.chimos.property.annotation.Property;
import de.chimos.property.compiler.Config;
import de.chimos.property.compiler.PropertyInfo;

/**
 * @author Niklas Hofmann
 */
public class PassTwoJFXClassVisitor extends ClassVisitor {

    private String className;
    
    private final Map<String, Map<String, PropertyInfo>> properties;
    
    private final Map<String, Boolean> getterMethodFound = new HashMap<>();
    private final Map<String, Boolean> setterMethodFound = new HashMap<>();
    
    private final Config config;
    
    public PassTwoJFXClassVisitor(Map<String, Map<String, PropertyInfo>> properties, ClassVisitor cv, Config config)
    {
        super(ASM4, cv);
        
        this.properties = properties;
        
        this.config = config;
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces)
    {
        className = name;
        super.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value)
    {
        // Drop simple field
        if(properties.containsKey(className) == true && properties.get(className).containsKey(name) && properties.get(className).get(name).isCompiled == false)
        {
        	if(config.dummyFields == false)
        	{
	            System.out.println("Drop field "+className+"/"+name);
	            return null;
        	}
        	else
        	{
	            System.out.println("Drop annotations for "+className+"/"+name);
        		super.visitField(access, name, desc, signature, value);
        		return null;
        	}
        }
        
        return super.visitField(access, name, desc, signature, value);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions)
    {
        if(properties.containsKey(className))
        {
            for(PropertyInfo property : properties.get(className).values())
            {
            	if(property.isCompiled == true)
            		continue;
            	
                if(name.equals(property.propertyMethodName) || (property.readOnlyPropertyMethodName != null && name.equals(property.readOnlyPropertyMethodName)))
                {
                	System.out.println("Drop method "+name);
                    return null;
                }
                
                if(property.internalGetterMethodName != null && name.equals(property.internalGetterMethodName))
                {
                    throw new IllegalArgumentException("Internal getter method exists already.");
                }
                
                if(property.internalSetterMethodName != null && name.equals(property.internalSetterMethodName))
                {
                    throw new IllegalArgumentException("Internal setter method exists already.");
                }
                
                if(name.equals(property.getterMethodName))
                {
                    getterMethodFound.put(property.propertyName, Boolean.TRUE);
                    break;
                }
                
                if(name.equals(property.setterMethodName))
                {
                    setterMethodFound.put(property.propertyName, Boolean.TRUE);
                    break;
                }
            }
        }
        
        return new PassTwoJFXMethodVisitor
                (
                    className,
                    name,
                    properties,
                    super.visitMethod(access, name, desc, signature, exceptions),
                    config
                );
    }
    
    @Override
    public void visitEnd() {
        
        if(properties.containsKey(className))
        {
            for(PropertyInfo property : properties.get(className).values())
            {
            	if(property.isCompiled == true)
            		continue;
            	
                System.out.println("Applying property "+property.fullPropertyName);

                // Create property field
                System.out.println("Creating field "+property.propertyFieldName);
                cv.visitField(ACC_PRIVATE, property.propertyFieldName, property.propertyDataSignature, property.propertyDataSignatureGeneric, null);

                // Create property method
                System.out.println("Creating method "+property.propertyMethodName);
                generatePropertyMethod(property);

                if(property.readAccessLevel != property.writeAccessLevel)
                {
                    System.out.println("Creating method "+property.readOnlyPropertyMethodName);
                    generateReadOnlyPropertyMethod(property);
                }
                
                // Create getter method
                System.out.println("Creating method "+property.internalGetterMethodName);
                generateInternalGetterMethod(property);
                
                if(getterMethodFound.get(property.propertyName) != Boolean.TRUE)
                {
                    System.out.println("Creating method "+property.getterMethodName);
                    generateGetterMethod(property);
                }
                
                // Create setter method
                System.out.println("Creating method "+property.internalSetterMethodName);
                generateInternalSetterMethod(property);
                    
                if(setterMethodFound.get(property.propertyName) != Boolean.TRUE)
                {
                    System.out.println("Creating method "+property.setterMethodName);
                    generateSetterMethod(property);
                }
            }
        }
        
        super.visitEnd();
    }
    
    protected void generatePropertyMethod(PropertyInfo property)
    {
        MethodVisitor mv = cv.visitMethod
                (
                    property.writeAccessLevel+ACC_FINAL, property.propertyMethodName,
                    "()"+property.propertyInterfaceSignature,
                    property.propertyInterfaceSignatureGeneric!=null?"()"+property.propertyInterfaceSignatureGeneric:null,
                    null
                );
        {
            AnnotationVisitor av0 = mv.visitAnnotation("L"+Property.class.getName().replace('.','/')+";", true);
            av0.visit("writeable", Boolean.TRUE);
            av0.visit("name", property.propertyName);
            av0.visit("dataSignature", property.dataSignature);
            av0.visit("dataSignatureGeneric", property.dataSignatureGeneric!=null?property.dataSignatureGeneric:"");
            av0.visit("humanReadableName", property.humanReadablePropertyName!=null?property.humanReadablePropertyName:"");
            av0.visitEnd();
        }
        mv.visitCode();
        Label l0 = new Label();
        mv.visitLabel(l0);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, className, property.propertyFieldName, property.propertyDataSignature);
        Label l1 = new Label();
        mv.visitJumpInsn(IFNONNULL, l1);
        Label l2 = new Label();
        mv.visitLabel(l2);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitTypeInsn(NEW, property.propertyDataType);
        mv.visitInsn(DUP);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitLdcInsn(property.propertyName);
        mv.visitMethodInsn(INVOKESPECIAL, property.propertyDataType, "<init>", "(Ljava/lang/Object;Ljava/lang/String;)V");
        mv.visitFieldInsn(PUTFIELD, className, property.propertyFieldName, property.propertyDataSignature);
        mv.visitLabel(l1);
        mv.visitFrame(F_SAME, 0, null, 0, null);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, className, property.propertyFieldName, property.propertyDataSignature);
        mv.visitTypeInsn(CHECKCAST, property.propertyInterfaceType);
        mv.visitInsn(ARETURN);
        Label l3 = new Label();
        mv.visitLabel(l3);
        mv.visitLocalVariable("this", "L"+className+";", null, l0, l3, 0);
        mv.visitMaxs(5, 1);
        mv.visitEnd();
    }
    
    protected void generateReadOnlyPropertyMethod(PropertyInfo property)
    {
        MethodVisitor mv = cv.visitMethod
                (
                    property.readAccessLevel+ACC_FINAL, property.readOnlyPropertyMethodName,
                    "()"+property.readOnlyPropertyInterfaceSignature,
                    property.readOnlyPropertyInterfaceSignatureGeneric!=null?"()"+property.readOnlyPropertyInterfaceSignatureGeneric:null,
                    null
                );
        {
            AnnotationVisitor av0 = mv.visitAnnotation("L"+Property.class.getName().replace('.','/')+";", true);
            av0.visit("writeable", Boolean.FALSE);
            av0.visit("name", property.propertyName);
            av0.visit("dataSignature", property.dataSignature);
            av0.visit("dataSignatureGeneric", property.dataSignatureGeneric!=null?property.dataSignatureGeneric:"");
            av0.visit("humanReadableName", property.humanReadablePropertyName!=null?property.humanReadablePropertyName:"");
            av0.visitEnd();
        }
        mv.visitCode();
        Label l0 = new Label();
        mv.visitLabel(l0);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, className, property.propertyFieldName, property.propertyDataSignature);
        Label l1 = new Label();
        mv.visitJumpInsn(IFNONNULL, l1);
        Label l2 = new Label();
        mv.visitLabel(l2);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitTypeInsn(NEW, property.propertyDataType);
        mv.visitInsn(DUP);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitLdcInsn(property.propertyName);
        mv.visitMethodInsn(INVOKESPECIAL, property.propertyDataType, "<init>", "(Ljava/lang/Object;Ljava/lang/String;)V");
        mv.visitFieldInsn(PUTFIELD, className, property.propertyFieldName, property.propertyDataSignature);
        mv.visitLabel(l1);
        mv.visitFrame(F_SAME, 0, null, 0, null);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, className, property.propertyFieldName, property.propertyDataSignature);
        mv.visitMethodInsn(INVOKEVIRTUAL, property.propertyDataType, "getReadOnlyProperty", "()"+property.readOnlyPropertyInterfaceSignature);
        mv.visitTypeInsn(CHECKCAST, property.readOnlyPropertyInterfaceType);
        mv.visitInsn(ARETURN);
        Label l3 = new Label();
        mv.visitLabel(l3);
        mv.visitLocalVariable("this", "L"+className+";", null, l0, l3, 0);
        mv.visitMaxs(5, 1);
        mv.visitEnd();
    }
    
    protected void generateInternalGetterMethod(PropertyInfo property)
    {
        MethodVisitor mv = cv.visitMethod
                (
                    ACC_PRIVATE+ACC_FINAL,
                    property.internalGetterMethodName,
                    "()"+property.dataSignature,
                    property.dataSignatureGeneric!=null?"()"+property.dataSignatureGeneric:null,
                    null
                );
        mv.visitCode();
        Label l0 = new Label();
        mv.visitLabel(l0);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKEVIRTUAL, className, property.propertyMethodName, "()"+property.propertyInterfaceSignature);
        mv.visitMethodInsn(INVOKEVIRTUAL, property.propertyInterfaceType, "getValue", "()Ljava/lang/Object;");
        mv.visitTypeInsn(CHECKCAST, property.dataType);
        mv.visitInsn(ARETURN);
        Label l1 = new Label();
        mv.visitLabel(l1);
        mv.visitLocalVariable("this", "L"+className+";", null, l0, l1, 0);
        mv.visitMaxs(1, 1);
        mv.visitEnd();
    }
    
    protected void generateInternalSetterMethod(PropertyInfo property)
    {
        MethodVisitor mv = cv.visitMethod
                (
                    ACC_PRIVATE+ACC_FINAL,
                    property.internalSetterMethodName,
                    "("+property.dataSignature+")V",
                    property.dataSignatureGeneric!=null?"("+property.dataSignatureGeneric+")V":null,
                    null
                );
        mv.visitCode();
        Label l0 = new Label();
        mv.visitLabel(l0);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKEVIRTUAL, className, property.propertyMethodName, "()"+property.propertyInterfaceSignature);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitMethodInsn(INVOKEVIRTUAL, property.propertyInterfaceType, "setValue", "(Ljava/lang/Object;)V");
        Label l1 = new Label();
        mv.visitLabel(l1);
        mv.visitInsn(RETURN);
        Label l2 = new Label();
        mv.visitLabel(l2);
        mv.visitLocalVariable("this", "L"+className+";", null, l0, l2, 0);
        mv.visitLocalVariable("value", property.dataSignature, null, l0, l2, 1);
        mv.visitMaxs(2, 2);
        mv.visitEnd();
    }

    protected void generateGetterMethod(PropertyInfo property)
    {
        MethodVisitor mv = cv.visitMethod
                (
                    property.readAccessLevel,
                    property.getterMethodName,
                    "()"+property.dataSignature,
                    property.dataSignatureGeneric!=null?"()"+property.dataSignatureGeneric:null,
                    null
                );
        if(property.enableSerialization == true && config.serialization == Config.Serialization.JAXB)
        {
            AnnotationVisitor av0 = mv.visitAnnotation("Ljavax/xml/bind/annotation/XmlElement;", true);
            av0.visitEnd();
        }
        mv.visitCode();
        Label l0 = new Label();
        mv.visitLabel(l0);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKESPECIAL, className, property.internalGetterMethodName, "()"+property.dataSignature);
        mv.visitInsn(ARETURN);
        Label l1 = new Label();
        mv.visitLabel(l1);
        mv.visitLocalVariable("this", "L"+className+";", null, l0, l1, 0);
        mv.visitMaxs(1, 1);
        mv.visitEnd();
    }
    
    protected void generateSetterMethod(PropertyInfo property)
    {
        MethodVisitor mv = cv.visitMethod
                (
                    property.writeAccessLevel,
                    property.setterMethodName,
                    "("+property.dataSignature+")V",
                    property.dataSignatureGeneric!=null?"("+property.dataSignatureGeneric+")V":null,
                    null
                );
        mv.visitCode();
        Label l0 = new Label();
        mv.visitLabel(l0);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitMethodInsn(INVOKESPECIAL, className, property.internalSetterMethodName, "("+property.dataSignature+")V");
        Label l1 = new Label();
        mv.visitLabel(l1);
        mv.visitInsn(RETURN);
        Label l2 = new Label();
        mv.visitLabel(l2);
        mv.visitLocalVariable("this", "L"+className+";", null, l0, l2, 0);
        mv.visitLocalVariable("value", property.dataSignature, null, l0, l2, 1);
        mv.visitMaxs(2, 2);
        mv.visitEnd();
    }

}
